(ns athens.listeners
  (:require
    [athens.common-db :as common-db]
    [athens.db :as db]
    [athens.electron.utils :as electron.utils]
    [athens.events.selection :as select-events]
    [athens.router :as router]
    [athens.subs.selection :as select-subs]
    [athens.util :as util]
    [clojure.string :as string]
    [goog.events :as events]
    [re-frame.core :as rf :refer [dispatch dispatch-sync subscribe]])
  (:import
    (goog.events
      EventType
      KeyCodes)))


(defn multi-block-selection
  "When blocks are selected, handle various keypresses:
  - shift+up/down: increase/decrease selection.
  - enter: deselect and begin editing textarea
  - backspace: delete all blocks
  - up/down: change editing textarea
  - tab: indent/unindent blocks
  Can't use textarea-key-down from keybindings.cljs because textarea is no longer focused."
  [e]
  (let [selected-items @(subscribe [::select-subs/items])]
    (when (not-empty selected-items)
      (let [shift    (.. e -shiftKey)
            key-code (.. e -keyCode)
            enter?   (= key-code KeyCodes.ENTER)
            bksp?    (= key-code KeyCodes.BACKSPACE)
            up?      (= key-code KeyCodes.UP)
            down?    (= key-code KeyCodes.DOWN)
            tab?     (= key-code KeyCodes.TAB)
            delete?  (= key-code KeyCodes.DELETE)]
        (cond
          enter? (do
                   (dispatch [:editing/uid (first selected-items)])
                   (dispatch [::select-events/clear]))
          (or bksp? delete?)  (do
                                (dispatch [::select-events/delete])
                                (dispatch [::select-events/clear]))
          tab? (do
                 (.preventDefault e)
                 (if shift
                   (dispatch [:unindent/multi {:uids selected-items}])
                   (dispatch [:indent/multi {:uids selected-items}])))
          (and shift up?) (dispatch [:selected/up selected-items])
          (and shift down?) (dispatch [:selected/down selected-items])
          (or up? down?) (do
                           (.preventDefault e)
                           (dispatch [::select-events/clear])
                           (if up?
                             (dispatch [:up (first selected-items) e])
                             (dispatch [:down (last selected-items) e]))))))))


(defn unfocus
  "Clears editing/uid when user clicks anywhere besides bullets, header, or on a block.
  Clears selected/items when user clicks somewhere besides a bullet point."
  [e]
  (let [selected-items?      (not-empty @(subscribe [::select-subs/items]))
        editing-uid          @(subscribe [:editing/uid])
        closest-block        (.. e -target (closest ".block-content"))
        closest-block-header (.. e -target (closest ".block-header"))
        closest-page-header  (.. e -target (closest ".page-header"))
        closest-bullet       (.. e -target (closest ".anchor"))
        closest-dropdown     (.. e -target (closest "#dropdown-menu"))
        closest-context-menu (.. e -target (closest ".app-context-menu"))
        closest              (or closest-block closest-block-header closest-page-header closest-dropdown closest-context-menu)]
    (when (and selected-items?
               (nil? (or closest-bullet closest-context-menu)))
      (dispatch [::select-events/clear]))
    (when (and (nil? closest)
               editing-uid)
      (dispatch [:editing/uid nil]))))


;; -- Hotkeys ------------------------------------------------------------


(defn key-down!
  [e]
  (let [{:keys [key-code
                ctrl
                meta
                shift
                alt]
         :as   destruct-keys} (util/destruct-key-down e)
        editing-uid           @(subscribe [:editing/uid])
        window-uid            (or @(subscribe [:current-route/uid-compat])
                                  (when (= @(subscribe [:current-route/name]) :home)
                                    ;; On daily notes, assume you're on the first note.
                                    (-> @(subscribe [:daily-notes/items])
                                        first)))]
    (cond
      (and (nil? editing-uid)
           window-uid
           (= key-code KeyCodes.UP))     (dispatch [:editing/last-child window-uid])

      (and (nil? editing-uid)
           window-uid
           (= key-code KeyCodes.DOWN))   (dispatch [:editing/first-child window-uid])

      (util/navigate-key? destruct-keys) (condp = key-code
                                           KeyCodes.LEFT  (when (nil? editing-uid)
                                                            (.back js/window.history))
                                           KeyCodes.RIGHT (when (nil? editing-uid)
                                                            (.forward js/window.history))
                                           nil)
      (util/shortcut-key? meta ctrl)     (condp = key-code
                                           KeyCodes.S         (dispatch [:save])
                                           KeyCodes.EQUALS    (dispatch [:zoom/in])
                                           KeyCodes.DASH      (dispatch [:zoom/out])
                                           KeyCodes.ZERO      (dispatch [:zoom/reset])
                                           KeyCodes.K         (do
                                                                (dispatch [:athena/toggle])
                                                                (.. e preventDefault))
                                           KeyCodes.Z         (do
                                                                ;; Disable the default undo behaviour.
                                                                ;; Chrome has a textarea undo that does not behave like
                                                                ;; we want undo to behave.
                                                                (.. e preventDefault)
                                                                ;; Dispatch our custom undo/redo.
                                                                (if shift
                                                                  (dispatch [:redo])
                                                                  (dispatch [:undo])))
                                           KeyCodes.O         (do
                                                                ;; Disable the default "Open file..." behaviour.
                                                                ;; We use this for navigation instead.
                                                                (.. e preventDefault)
                                                                (when alt
                                                                  ;; When alt is also pressed, zoom out of current block page
                                                                  (when-let [parent-uid (->> [:block/uid @(subscribe [:current-route/uid])]
                                                                                             (common-db/get-parent-eid @db/dsdb)
                                                                                             second)]
                                                                    (rf/dispatch [:reporting/navigation {:source :kbd-ctrl-alt-o
                                                                                                         :target :block
                                                                                                         :pane   (if shift
                                                                                                                   :right-pane
                                                                                                                   :main-pane)}])
                                                                    (router/navigate-uid parent-uid e))))
                                           KeyCodes.BACKSLASH (if shift
                                                                (dispatch [:right-sidebar/toggle])
                                                                (dispatch [:left-sidebar/toggle]))
                                           KeyCodes.COMMA     (do
                                                                (rf/dispatch [:reporting/navigation {:source :kbd-ctrl-comma
                                                                                                     :target :settings
                                                                                                     :pane   :main-pane}])
                                                                (router/navigate :settings))
                                           KeyCodes.T         (util/toggle-10x)
                                           nil)
      alt                                (condp = key-code
                                           KeyCodes.D (do
                                                        (rf/dispatch [:reporting/navigation {:source :kbd-alt-d
                                                                                             :target :home
                                                                                             :pane   :main-pane}])
                                                        (router/nav-daily-notes)
                                                        (.. e preventDefault))
                                           KeyCodes.G (do
                                                        (rf/dispatch [:reporting/navigation {:source :kbd-alt-g
                                                                                             :target :graph
                                                                                             :pane   :main-pane}])
                                                        (router/navigate :graph))
                                           KeyCodes.A (do
                                                        (rf/dispatch [:reporting/navigation {:source :kbd-alt-a
                                                                                             :target :all-pages
                                                                                             :pane   :main-pane}])
                                                        (router/navigate :pages))
                                           KeyCodes.T (dispatch [:theme/toggle])
                                           nil))))


;; -- Clipboard ----------------------------------------------------------

(defn unformat-double-brackets
  "https://github.com/ryanguill/roam-tools/blob/eda72040622555b52e40f7a28a14744bce0496e5/src/index.js#L336-L345"
  [s]
  (-> s
      (string/replace #"\[([^\[\]]+)\]\((\[\[|\(\()([^\[\]]+)(\]\]|\)\))\)" "$1")
      (string/replace #"\[\[([^\[\]]+)\]\]" "$1")))


(defn block-refs-to-plain-text
  "If there is a valid ((uid)), find the original block's string.
  If invalid ((uid)), no-op.
  TODO: If deep block ref, convert deep block ref to plain-text.

  Want to put this in athens.util, but circular dependency from athens.db"
  [s]
  (let [replacements (->> s
                          (re-seq #"\(\(([^\(\)]+)\)\)")
                          (map (fn [[orig-str match-str]]
                                 (let [eid (db/e-by-av :block/uid match-str)]
                                   (if eid
                                     [orig-str (str "((" (db/v-by-ea eid :block/string) "))")]
                                     [orig-str (str "((" match-str "))")])))))]
    (loop [replacements replacements
           s            s]
      (let [[orig-str replace-str] (first replacements)]
        (if (empty? replacements)
          s
          (recur (rest replacements)
                 (clojure.string/replace s orig-str replace-str)))))))


(defn blocks-to-clipboard-data
  "Four spaces per depth level."
  ([depth node]
   (blocks-to-clipboard-data depth node false))
  ([depth node unformat?]
   (let [{:block/keys [string
                       children
                       _header]} node
         left-offset             (apply str (repeat depth "    "))
         walk-children           (apply str (map #(blocks-to-clipboard-data (inc depth) % unformat?)
                                                 children))
         string                  (if unformat?
                                   (-> string
                                       unformat-double-brackets
                                       block-refs-to-plain-text)
                                   (block-refs-to-plain-text string))
         dash                    (if unformat? "" "- ")]
     (str left-offset dash string "\n" walk-children))))


(defn copy
  "If blocks are selected, copy blocks as markdown list.
  Use -event_ because goog events quirk "
  [^js e]
  (let [uids @(subscribe [::select-subs/items])]
    (when (not-empty uids)
      (let [uids           (mapv (comp first db/uid-and-embed-id) uids)
            copy-data      (->> uids
                                (map #(common-db/get-block-document @db/dsdb [:block/uid %]))
                                (map #(blocks-to-clipboard-data 0 %))
                                (apply str))
            clipboard-data (.. e -event_ -clipboardData)
            copied-blocks  (mapv
                             #(common-db/get-internal-representation  @db/dsdb [:block/uid %])
                             uids)]

        (doto clipboard-data
          (.setData "text/plain" copy-data)
          (.setData "application/athens-representation" (pr-str copied-blocks))
          (.setData "application/athens" (pr-str {:uids uids})))
        (.preventDefault e)))))


(defn cut
  "Cut is essentially copy AND delete selected blocks"
  [^js e]
  (let [uids @(subscribe [::select-subs/items])]
    (when (not-empty uids)
      (copy e)
      (dispatch [::select-events/delete]))))


(def force-leave (atom false))


(defn prevent-save
  "Google Closure's events/listen isn't working for some reason anymore.

  beforeunload is called before unload, where the window would be redirected/refreshed/quit.
  https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event "
  []
  (js/window.addEventListener
    EventType.BEFOREUNLOAD
    (fn [e]
      (let [synced? @(subscribe [:db/synced])
            ;; See test/e2e/electron-test.ts for details about this flag.
            e2e-ignore-save? (= (js/localStorage.getItem "E2E_IGNORE_SAVE") "true")
            remote? (electron.utils/remote-db? @(subscribe [:db-picker/selected-db]))]
        (cond
          (and (not synced?)
               (not @force-leave)
               (not e2e-ignore-save?))
          (do
            ;; The browser blocks the confirm window during beforeunload, so
            ;; instead we always cancel unload and separately show a confirm window
            ;; that allows closing the window.
            (dispatch [:confirm/js
                       (str "Athens hasn't finished saving yet. Athens is finished saving when the sync dot is green. "
                            "Try refreshing or quitting again once the sync is complete. "
                            "Press Cancel to wait, or OK to leave without saving (will cause data loss!).")
                       (fn []
                         (reset! force-leave true)
                         (js/window.close))
                       #()])
            (.. e preventDefault)
            (set! (.. e -returnValue) "Setting e.returnValue to string prevents exit for some browsers.")
            "Returning a string also prevents exit on other browsers.")

          remote?
          (dispatch-sync [:remote/disconnect!]))))))


(defn init
  []
  (events/listen js/document EventType.MOUSEDOWN unfocus)
  (events/listen js/window EventType.KEYDOWN multi-block-selection)
  (events/listen js/window EventType.KEYDOWN key-down!)
  (events/listen js/window EventType.COPY copy)
  (events/listen js/window EventType.CUT cut)
  (prevent-save))
